/*
 * Copyright 2007, MetaDimensional Technologies Inc.
 *
 *
 * This file is part of the RememberTheMilk Java API.
 *
 * The RememberTheMilk Java API is free software; you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * The RememberTheMilk Java API is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package ch.lab24.rtm;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.HttpStatus;
import org.apache.http.util.ByteArrayBuffer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import ch.lab24.android.rtm.http.LoadListenerEventHandler;
import ch.lab24.android.rtm.http.RequestHandle;
import ch.lab24.android.rtm.http.RequestQueue;



import android.content.Context;
import android.net.Uri;

/**
 * Handles the details of invoking a method on the RTM REST API.
 * 
 * @author Will Ross Jun 21, 2007
 */
public class Invoker {

	private static final DocumentBuilder builder;
	static {
		DocumentBuilder b;
		try {
			DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
			fact.setValidating(false);
			fact.setNamespaceAware(false);
			b = fact.newDocumentBuilder();	
			
		} catch (Exception e) {
			b = null;
		}
		builder = b;
	}

	private final String serviceHost;

	public static final String REST_SERVICE_URL_POSTFIX = "/services/rest/";

	public static final String ENC = "UTF-8";

	public static String API_SIG_PARAM = "api_sig";

	public static final long INVOCATION_INTERVAL = 2000;

	private long lastInvocation;

	private final ApplicationInfo applicationInfo;

	private final MessageDigest digest;

	private String proxyHostName;

	private int proxyPortNumber;
	
	private Context mContext;


	public Invoker(String serviceBaseUrl, ApplicationInfo applicationInfo, Context context)
			throws ServiceInternalException {
		this.serviceHost = serviceBaseUrl;
		this.mContext = context;
		lastInvocation = System.currentTimeMillis();
		this.applicationInfo = applicationInfo;
		
		try {
			digest = MessageDigest.getInstance("md5");
		} catch (NoSuchAlgorithmException e) {
			throw new ServiceInternalException(
					"Could not create properly the MD5 digest", e);
		}
	}

	public void setHttpProxySettings(String proxyHostName, int proxyPortNumber) {
		this.proxyHostName = proxyHostName;
		this.proxyPortNumber = proxyPortNumber;
	}

	public Element invoke(Param... params) throws ServiceException {
		Element result;

		long timeSinceLastInvocation = System.currentTimeMillis()
				- lastInvocation;
		if (timeSinceLastInvocation < INVOCATION_INTERVAL) {
			// In order not to invoke the RTM service too often
			try {
				Thread.sleep(INVOCATION_INTERVAL - timeSinceLastInvocation);
			} catch (InterruptedException e) {
				throw new ServiceInternalException(
						"Unexpected interruption while attempting to pause for some time before invoking the RTM service back",
						e);
			}
		}
		

		final Uri.Builder uri = new Uri.Builder();
        uri.path(REST_SERVICE_URL_POSTFIX);
		for (Param param : params) {
	        uri.appendQueryParameter(param.getName(), param.getValue());
		}
        uri.appendQueryParameter(API_SIG_PARAM, calcApiSig(params));

       	RequestQueue queue = new RequestQueue(mContext);
    	HashMap<String, String> mMap = new HashMap<String, String>();
    	LoadListenerEventHandler eventHandler = new LoadListenerEventHandler();
    	RequestHandle handle =  queue.queueRequest(serviceHost + uri.build().toString()
    				, "GET", mMap, eventHandler, null, 0, true);
    	handle.waitUntilComplete();
        queue.shutdown();
		try {
			String vRequestUrl = serviceHost + uri.build().toString();
		   	String vStringResult = new String(eventHandler.getData(), "UTF-8");

			int statusCode = eventHandler.getStatusCode();
			if (statusCode != HttpStatus.SC_OK) {
				throw new ServiceInternalException("method failed: ");
			}


			Document responseDoc = builder.parse(new ByteArrayInputStream(eventHandler.getData()));
            
						
			Element wrapperElt = responseDoc.getDocumentElement();
			if (!wrapperElt.getNodeName().equals("rsp")) {
				throw new ServiceInternalException(
						"unexpected response returned by RTM service: ");
			} else {
				String stat = wrapperElt.getAttribute("stat");
				if (stat.equals("fail")) {
					Node errElt = wrapperElt.getFirstChild();
					while (errElt != null
							&& (errElt.getNodeType() != Node.ELEMENT_NODE || !errElt
									.getNodeName().equals("err"))) {
						errElt = errElt.getNextSibling();
					}
					if (errElt == null) {
						throw new ServiceInternalException(
								"unexpected response returned by RTM service:");
					} else {
						throw new ServiceException(Integer
								.parseInt(((Element) errElt)
										.getAttribute("code")),
								((Element) errElt).getAttribute("msg"));
					}
				} else {
					Node dataElt = wrapperElt.getFirstChild();
					while (dataElt != null
							&& (dataElt.getNodeType() != Node.ELEMENT_NODE || dataElt
									.getNodeName().equals("transaction") == true)) {
						try {
							Node nextSibling = dataElt.getNextSibling();
							if (nextSibling == null) {
								break;
							} else {
								dataElt = nextSibling;
							}
						} catch (IndexOutOfBoundsException exception) {
							// Some implementation may throw this exception,
							// instead of returning a null sibling
							break;
						}
					}
					if (dataElt == null) {
						throw new ServiceInternalException(
								"unexpected response returned by RTM service:");
					} else {
						result = (Element) dataElt;
					}
				}
			}
		} catch (IOException e) {
			throw new ServiceInternalException("", e);
		} catch (SAXException e) {
			throw new ServiceInternalException("", e);
		}
		lastInvocation = System.currentTimeMillis();
		return result;
	}
 

	final String calcApiSig(Param... params) throws ServiceInternalException {
		try {
			digest.reset();
			digest.update(applicationInfo.getSharedSecret().getBytes(ENC));
			List<Param> sorted = Arrays.asList(params);
			Collections.sort(sorted);
			for (Param param : sorted) {
				digest.update(param.getName().getBytes(ENC));
				digest.update(param.getValue().getBytes(ENC));
			}
			return encode(digest.digest());
			// return new String(digest.digest(), ENC);
		} catch (UnsupportedEncodingException e) {
			throw new ServiceInternalException(
					"cannot hahdle properly the encoding", e);
		}
	}

	protected static final byte[] Hexhars = {
	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
			'f' };

	private static String encode(byte[] b) {

		StringBuilder s = new StringBuilder(2 * b.length);

		for (int i = 0; i < b.length; i++) {

			int v = b[i] & 0xff;

			s.append((char) Hexhars[v >> 4]);
			s.append((char) Hexhars[v & 0xf]);
		}

		return s.toString();
	}
	

	private byte[] convertStreamToByte(InputStream is) {
        byte[] buffer = new byte[256];
        ByteArrayBuffer vByteBuffer = new ByteArrayBuffer(256);
        try {
             while (is.read(buffer, 0, 256) != -1) {
                  vByteBuffer.append(buffer, 0, 256);
             }
        } catch (IOException e) {
             e.printStackTrace();
        } catch (Exception e) {
        	e.printStackTrace();        
        } finally {
        }
        return vByteBuffer.toByteArray();
   }
	
}
